/*
 * Copyright (c) 2007 Universidade Federal de Campina Grande
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package br.edu.ufcg.threadcontrol;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;



/**
 * This class handles threads states.
 */
public class ThreadWatcher {

	/*
	 * Improve this solution. Maybe using just one list of threads that keep
	 * their state. However, this solution may not be very efficient.
	 */
	
	private ThreadManager threadManager = new ThreadManager();
	/**
	 * The objects where a wait was called.
	 */
	private Map<Object,MonitoredObject> monitoredObjects = new HashMap<Object,MonitoredObject>();

	private Object controllerLock = new Object();
	private boolean stateReached = false;
	private boolean isSituationBeingExpected = false;

	private List<ThreadConfiguration> threadsConfiguration;

	/**
	 * A debug variable.
	 */
	private static final boolean DEBUG = false;

	/**
	 * Used to log when devug is on.
	 */
	private static transient final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger( ThreadWatcher.class );


	/**
	 * Default constructor.
	 */
	public ThreadWatcher() {

	}


	/**
	 * Waits until all running threads die. This method deserves more tests.
	 */
	public synchronized void waitUntilAllThreadsDie() {

		if ( areAllThreadsDead()) {
			this.println( "No threads are running" );
			return;
		}
		this.printAllThreads();
		this.println( "!!Will wait all threads die:" + Thread.currentThread() );
		while ( !areAllThreadsDead()) {
			try {
				wait();
				this.printAllThreads();
			} catch ( InterruptedException e ) {
				this.println( "Thread interrupted " + e );
			}
		}
		this.println( "!!Waited all threads die" );
	}
	//TODO: Maybe move to ThreadManager
	private boolean areAllThreadsDead(){
		return !this.threadManager.areThereThreadsRunning() && !this.threadManager.areThereThreadsStarted() && !this.threadManager.areThereThreadsWaiting() ;
	}

	/**
	 * Prints all threads running, started and waiting.
	 */
	private void printAllThreads() {

		printRunningThreads();
		printStartedThreads();
		printWaitingThreads();
	}


	/**
	 * This method puts the current running thread to wait until all application
	 * running threads are waiting.
	 */
	public synchronized void waitUntilWorkIsDoneNotifying() {

		if ( this.areAllRunningThreadsWaiting() ) {
			this.println( "All are waiting. Thread will continue" );
			return;
		}
		this.println( "++This thread will wait until work is done:" + Thread.currentThread() );
		while ( !this.areAllRunningThreadsWaiting() ) {
			try {
				wait();
			} catch ( InterruptedException e ) {
				this.println( "Thread interrupted " + e );
			}
		}
		this.println( "++Thread waited until work is done:" + Thread.currentThread() );
	}


	/**
	 * Puts the current thread to wait until a certain Thread t leaves the run
	 * method.
	 * 
	 * @param t The thread we must wait until it stops running.
	 */
	public synchronized void waitUntilThreadHasFinished( Thread t ) {

		if ( !this.isThreadStarted( t ) && !this.isThreadRunning( t ) ) {
			this.println( "This thread has not started yet. We will not wait it stop running.Thread:" + t );
			return;
		}
		this.println( "++=>Will wait thread stops running:" + Thread.currentThread() );
		while ( isThreadStarted( t ) || isThreadRunning( t ) ) {
			try {
				wait();
			} catch ( InterruptedException e ) {
				this.println( "Thread interrupted " + e );
			}
		}
		this.println( "++=>Waited thread stops running:" + Thread.currentThread() );
	}


	/**
	 * Waits until a given thread enters the run state. Attention: Use this
	 * method when you know it will not leave the running state until you call
	 * it. It is more usefull in service threads. Due to the aspects, before any
	 * line of the run method, a thread will be included in the runningThreads.
	 * Use this method combined with waitUntilThreadIsWaiting when you have
	 * service threads.
	 * 
	 * @param t The thread you want to wait for.
	 */
	public synchronized void waitUntilThreadHasStartedRunning( Thread t ) {
		if ( !threadManager.isThreadInState(t, ThreadConfiguration.STATE_STARTED) && !threadManager.isThreadInState(t, ThreadConfiguration.STATE_RUNNING) ) {
			this.println( "The thread " + t + " has not started. We will not wait until it is running" );
			return;
		}
		this.println( "++Thread <<" + Thread.currentThread() + ">> will wait until the following thread is running:"
				+ t );
		while ( !this.isThreadRunning( t ) ) {
			try {
				wait();
			} catch ( InterruptedException e ) {
				this.println( "Thread interrupted " + e );
			}
		}
		this.println( "++Thread <<" + Thread.currentThread() + ">> waited the following thread run:" + t );
	}



	/**
	 * Waits until a given thread is waiting.
	 * 
	 * @param t The thread you want to wait for.
	 */
	public synchronized void waitUntilThreadIsWaiting( Thread t ) {

		while ( !isThreadWaiting( t ) ) {
			try {
				wait();
			} catch ( InterruptedException e ) {
				this.println( "Thread interrupted " + e );
			}
		}
	}


	/**
	 * Verifies if a given thread is running (it has started its run method and
	 * has not leaved it. Notice that it may also be waiting).
	 * 
	 * @param t The wanted thread
	 * @return true if t is running.
	 */
	public synchronized boolean isThreadRunning( Thread t ) {

		return (this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_RUNNING) || this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_WAITING));
	}


	/**
	 * Verifies if a given thread is started, but not running yet
	 * 
	 * @param t The wanted thread
	 * @return true if t is started, but not running yet.
	 */
	public synchronized boolean isThreadStarted( Thread t ) {

		return (this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_STARTED));
	}


	/**
	 * Verifies if a given thread is waiting
	 * 
	 * @param t The wanted thread
	 * @return true if t is waiting.
	 */
	public synchronized boolean isThreadWaiting( Thread t ) {

		return (this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_WAITING));
	}


	/**
	 * Include in running threads a given thread.
	 * 
	 * @param t The thread to be included.
	 */
	public synchronized void includeInRunningThreads( Thread t ) {
		if (!this.isThreadRunning(t)) {
			boolean included = this.threadManager.changeState(t, ThreadConfiguration.STATE_STARTED, ThreadConfiguration.STATE_RUNNING);
			printMessageIncluding( t, included, "runningThreads" );
			this.notifyThreadsStateChange();
		}

	}


	/**
	 * Include in running threads a given thread.
	 * 
	 * @param t The thread to be included.
	 */
	public synchronized void includeInStartedThreads( Thread t ) {

		boolean included = this.threadManager.add(t, ThreadConfiguration.STATE_STARTED);
		printMessageIncluding( t, included, "startedThreads" );
		this.notifyThreadsStateChange();
	}
	
	public synchronized void includeInSleepingThreads( Thread t ) {
		this.threadManager.changeState(t, ThreadConfiguration.STATE_RUNNING, ThreadConfiguration.STATE_SLEEPING);
		this.notifyThreadsStateChange();
	}
	
	public synchronized void removeFromSleepingThreads( Thread t ) {
		this.threadManager.changeState(t, ThreadConfiguration.STATE_SLEEPING, ThreadConfiguration.STATE_RUNNING);
		this.notifyThreadsStateChange();
	}

	/**
	 * Removes from running threads a given thread
	 * 
	 * @param t The thread to be removed.
	 */
	private synchronized void removeFromRunningThreads( Thread t ) {

		boolean removed = this.threadManager.remove(t, ThreadConfiguration.STATE_RUNNING);
		printMessageRemoving( t, removed );
		this.threadManager.add(t, ThreadConfiguration.STATE_FINISHED);
		this.notifyThreadsStateChange();
	}


	/**
	 * Adds to the list of monitored objects a given object and the thread it is
	 * waiting for.
	 * 
	 * @param o The object where the wait was called.
	 * @param t The thread being executed when the wait was called.
	 */
	private void addToMonitoredObjects( Object o, Thread t ) {

		boolean added = false;
		MonitoredObject mo = monitoredObjects.get( o );
		if ( mo == null ) {
			monitoredObjects.put( o, new MonitoredObject( o, t ) );
			added = true;
		} else {
			added = mo.addThread( t );
		}
		this.println( "@@@@>Added:" + added + " O:" + o + " Thread:" + t );
	}


	/**
	 * Removes from the monitored objects a given object because a notify was
	 * called.
	 * 
	 * @param o the object where the <code>wait </code> was called.
	 */
	private void removeFromMonitoredObjects( Object o ) {

		Object r = monitoredObjects.remove( o );
		this.println( "==>Object removed " + r );
	}


	/**
	 * Counts the number of objects monitored by a given thread.
	 * 
	 * @param t The thread.
	 * @return the number of objects monitored by a given thread. It must always
	 *         be "one", verify when this does not happen.
	 */
	private int countObjectsMonitoredByThread( Thread t ) {

		int cont = 0;
		Iterator it = monitoredObjects.values().iterator();
		while ( it.hasNext() ) {
			MonitoredObject mo = (MonitoredObject) it.next();
			if ( mo.isMonitoredBy( t ) ) {
				cont++;
			}
		}
		return cont;
	}


	/**
	 * Prints a message when the notify or notifyAll method is called on an
	 * object and a thread is removed from the running threads.
	 * 
	 * @param t The thread.
	 * @param removed true if the set contained the specified element.
	 */
	private void printMessageRemoving( Thread t, boolean removed ) {

		if ( removed ) {
			this.println( "----Removing Running Thread:" + t );
		} else {
			this.println( "----Thread:" + t + " has already been removed from runningThreads" );
		}
	}


	/**
	 * Prints a message when a thread is included in the running threads. This
	 * happens when the start method is called and when the run method is
	 * executed, bringing two possible different messages.
	 * 
	 * @param t The thread being included.
	 * @param included true if this set did not already contain the specified
	 *        element, false otherwise.
	 */
	private void printMessageIncluding( Thread t, boolean included, String listName ) {

		if ( included ) {
			this.println( "||||Including a new Thread in " + listName + ":" + t );
		} else {
			this.println( "||||Thread:" + t + " has already been included in " + listName );
		}
	}


	/**
	 * Includes in the list of waiting threads a certain thread.
	 * 
	 * @param t The thread to be included.
	 */
	private synchronized void includeInWaitingThreads( Thread t ) {
		this.threadManager.changeState(t, ThreadConfiguration.STATE_RUNNING, ThreadConfiguration.STATE_WAITING);
		this.notifyThreadsStateChange();
	}


	/**
	 * Removes a certain thread from the list of waiting threads.
	 * 
	 * @param t The thread to be removed.
	 *///FIXME: Rename this method
	protected synchronized void removeFromWaitingThreads( Thread t, String toState ) {
		this.verifyThread();
		String previousState = threadManager.getThreadState(t);
		this.threadManager.changeState(t, previousState, toState);
		this.println( "****Finishing to wait:" + t );
		this.notifyThreadsStateChange();
	}


	/**
	 * Adds the current thread on the list of threads waiting for a certain
	 * object.
	 * 
	 * @param o The waited object.
	 */
	public synchronized void addWaiting( Object o ) {

		Thread t = Thread.currentThread();
		if ( this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_RUNNING) ) {
			includeInWaitingThreads( t );
			addToMonitoredObjects( o, t );
		} else {
			this.println( "Thread waiting is not aspected: object=" + o + " thread=" + t );
		}
	}

	public synchronized void threadFinishedToWaitOnObject( Object o, boolean timedWait ) {

		Thread t = Thread.currentThread();
//		this.vefifyIfConfigurationWasReached(this.threadsConfiguration);
//		this.verifyThread();
		if ( this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_WAITING) 
				|| this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_NOTIFIED)
						|| this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_POSSIBLY_NOTIFIED)) {
			String threadPreviousState = threadManager.getThreadState(t);
			synchronized ( monitoredObjects ) {
				MonitoredObject mo = monitoredObjects.get( o );
				threadManager.changeState(t, threadPreviousState, ThreadConfiguration.STATE_RUNNING );
				if (mo !=null) {
					if (mo.getMonitoringThreads().size()==1 && mo.getMonitoringThreads().contains(t)){
						removeFromMonitoredObjects( o );
					} else {
						mo.removeMonitoringThread(t);
						if (threadPreviousState.equals(ThreadConfiguration.STATE_POSSIBLY_NOTIFIED)){
							if (!timedWait){
								makePossiblyNotifiedThreadsWaitAgain(mo.getMonitoringThreads());
							} else {
								//FIXME: Verify what to do when we do not know if 
								//the thread has waked up due to a notify or due to
								//elapsed time. This is not so critical because using
								//only notify when more than one thread can be waiting is not
								//a good programming practice.
							}
						} 
					}
				}
				this.notifyThreadsStateChange();
			}
		} else if ( this.threadManager.isThreadInState(t, ThreadConfiguration.STATE_RUNNING) ) {
			this.println( "Thread was already running" );
		}else {
			boolean changed = this.threadManager.changeToState(t, ThreadConfiguration.STATE_RUNNING);
			if (changed){
				this.notifyThreadsStateChange();
			} else {
				this.println( "Thread was already running" );
			}
		}
	}

	private boolean makePossiblyNotifiedThreadsWaitAgain(Collection<Thread> possiblyNotifiedThreads) {
		boolean changed = false;
		for (Thread t: possiblyNotifiedThreads){
			String state = threadManager.getThreadState(t);
			if (state.equals(ThreadConfiguration.STATE_POSSIBLY_NOTIFIED)){
				changed = changed || this.threadManager.changeToState(t, ThreadConfiguration.STATE_WAITING);
			}
		}
		return changed;
		
	}


	/**
	 * Notifies all threads waiting for an object o
	 * 
	 * @param o The object where a notifyAll was called.
	 */
	public synchronized void notifyAllWaitingThreads( Object o ) {

		synchronized ( monitoredObjects ) {
			MonitoredObject mo = monitoredObjects.get( o );
			if ( mo != null ) {
				Iterator it = mo.getMonitoringThreadsIterator();
				while ( it.hasNext() ) {
					Thread t = (Thread) it.next();
					int objectsMonitored = countObjectsMonitoredByThread( t );
					if ( objectsMonitored == 1 ) {
						removeFromWaitingThreads( t, ThreadConfiguration.STATE_NOTIFIED );
					} else {
						this.println( "Strange: A Thread was monitoring more than one object" );
					}
				}
			} else {
				this.println( "NOTIFY LOST: No application thread was waiting on object #" + o
						+ "# and notify all was called" );
			}
		}
	}


	/**
	 * Notifies a thread that is waiting for an object o
	 * 
	 * @param o The object where a notify was called.
	 */
	public synchronized void notifyOneWaitingThread( Object o ) {
		synchronized ( monitoredObjects ) {
			MonitoredObject mo = monitoredObjects.get( o );
			if ( mo != null ) {
				Iterator it = mo.getMonitoringThreadsIterator();
				boolean moreThanOneThreadWaitingNotification = false;
				if ( mo.getMonitoringThreads().size() > 1 ) {
					System.err.println("notify() method was called on object=" + o
							+ " , but more than one thread should be notified" );
					moreThanOneThreadWaitingNotification = true;
				} 
				while ( it.hasNext() ) {
					Thread t = (Thread) it.next();
					int objectsMonitored = countObjectsMonitoredByThread( t );
					if ( objectsMonitored == 1 ) {
						String toState;
						if (moreThanOneThreadWaitingNotification){
							toState = ThreadConfiguration.STATE_POSSIBLY_NOTIFIED;
						} else {
							toState = ThreadConfiguration.STATE_NOTIFIED;
						}
						removeFromWaitingThreads( t, toState );
					} else {
						this.println( "Strange: A Thread was monitoring more than one object" );
					}
				}
			} else {
				this.println( "NOTIFY LOST: No application thread was waiting on object #" + o
						+ "# and notify was called" );
			}
		}
	}


	/**
	 * Prints the waiting threads.
	 */
	public synchronized void printWaitingThreads() {
		this.println( "WAITING THREADS:" );
		Iterator t = this.threadManager.getThreadsInState(ThreadConfiguration.STATE_WAITING).iterator();
		while ( t.hasNext() ) {
			println( "=>Thread:" + t.next() );
		}
	}


	/**
	 * Prints the started threads.
	 */
	public synchronized void printStartedThreads() {

		this.println( "STARTED THREADS:" );
		Iterator t = this.threadManager.getThreadsInState(ThreadConfiguration.STATE_STARTED).iterator();
		while ( t.hasNext() ) {
			println( "=>Thread:" + t.next() );
		}
	}


	/**
	 * Prints the running threads.
	 */
	public synchronized void printRunningThreads() {

		this.println( "RUNNING THREADS:" );
		Iterator t = this.threadManager.getThreadsInState(ThreadConfiguration.STATE_RUNNING).iterator();
		while ( t.hasNext() ) {
			println( "=>Thread:" + t.next() );
		}
	}


	/**
	 * Verifies if all running threads are waiting.
	 * 
	 * @return true if all running threads are waiting and false, otherwise.
	 */
	public synchronized boolean areAllRunningThreadsWaiting() {

		return !this.threadManager.areThereThreadsRunning() && !this.threadManager.areThereThreadsStarted();
	}


	/**
	 * Removes a runnable thread from the list.
	 */
	public synchronized void removeRunnableThread() {

		removeFromRunningThreads( Thread.currentThread() );
	}


	/**
	 * General method that prints the string parameter on the standard output.
	 * 
	 * @param str String to be printed.
	 */
	public void println( String str ) {

		if ( DEBUG ) {
			LOG.debug( str );
			System.out.println( str );
		}
	}
	
	/**
	 * @param threadsConfiguration
	 */
	public synchronized void waitUntilThreadConfiguration(List <ThreadConfiguration> threadsConfiguration) {
		this.threadsConfiguration = threadsConfiguration;
		this.isSituationBeingExpected = true;
		while (!this.stateReached){
			if (vefifyIfConfigurationWasReached(threadsConfiguration)){
				return;
			}
			try {
				this.wait();
				if (this.stateReached){
					return;
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public void prepare(List<ThreadConfiguration> threadsConfiguration){
		this.threadsConfiguration = threadsConfiguration;
		this.setSituationBeingExpected(true);
		this.stateReached = false;
	}

	/**
	 * @param threadsConfiguration
	 */
	private boolean vefifyIfConfigurationWasReached(List<ThreadConfiguration> threadsConfiguration) {
		if (threadsConfiguration == null || threadsConfiguration.size() == 0){
			this.setStateReached(true);
			return true;
		}
		boolean wasReached = false;
		for (ThreadConfiguration tc: threadsConfiguration){
			if (!threadManager.isThreadInState(tc.getThreadClassName(), tc.getExpectedStates(), tc.getTimesToBeInStateBeforeBlocking())){
				wasReached = false;
				break;
			} else {
				wasReached = true;
			}
		}
		if (wasReached){
			this.setStateReached(true);	
		}
		return wasReached;		
	}


	/**
	 * @param stateReached
	 */
	public void setStateReached(boolean stateReached) {
		this.stateReached = stateReached;	
		synchronized(this){
			this.notifyAll();
		}
	}

	public synchronized void notifyThreadsStateChange(){
		if (threadsConfiguration!=null){
			this.vefifyIfConfigurationWasReached(this.threadsConfiguration);
		}
		this.notifyAll();
		//Just in case the other waitUntil... methods are
		//being used (not the waitUntilConfiguration).
	}
	private void setSituationBeingExpected(boolean isSituationExpected){
		this.isSituationBeingExpected = isSituationExpected;
	}
	
	public boolean isSituationBeingExpected(){
		return this.isSituationBeingExpected;
	}
	
	public boolean isStateReached(){
		return stateReached;
	}
	
	public void proceed(){
		this.setStateReached(false);
		this.setSituationBeingExpected(false);
		synchronized (controllerLock){
			controllerLock.notifyAll();
		}
		synchronized (this){
			this.notifyAll();
		}
		
	}
	
	public void reset() {
		this.threadManager.reset();
		this.threadsConfiguration = null;
		this.stateReached = false;
		this.isSituationBeingExpected = false;
		this.monitoredObjects =  new HashMap<Object,MonitoredObject>();
	}


	public void verifyThread(){
		if (this.isStateReached() && isSituationBeingExpected()){
			try {
				synchronized(controllerLock){
					controllerLock.wait();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	

	public String getRunnableClassName(Thread t){
		return this.threadManager.getRunnableClassName(t);
	}
	public void addRunnableClassName(Thread t, String source){
		boolean wasAdded = this.threadManager.addRunnableClassName(t, source);
		if (wasAdded){
			this.notifyThreadsStateChange();
		}
	}


	protected void removeFromWaitingThreads(Thread t) {
		this.removeFromWaitingThreads(t, ThreadConfiguration.STATE_RUNNING);
		
	}
}
